package com.ian.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;
import java.util.function.Consumer;

import static org.springframework.security.config.Customizer.withDefaults;

/**
 * 覆盖默认的oauth2代码配置，以及覆盖了 application.yml 中的配置
 * enable OAuth 2.0 login through httpSecurity.oauth2Login()
 * 注：该类可以不存在，也能正常使用oauth2
 *
 * @author Witt
 * @version 1.0.0
 * @date 2022/5/16
 */
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize
                        // 配置放行 redirect_uri
                        .antMatchers("/getClientRegistration/**", "/login", "/login/oauth2", "/authorization_code/**").permitAll()
                        .anyRequest().authenticated()
                )
//                .formLogin(formLogin -> formLogin.loginPage("/login").permitAll())
                /* 注意：必须要配置oauth2Login()，否则页面不会重定向到 登录页面 /login，并且访问其他uri报错：
                        Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed;
                        nested exception is java.lang.IllegalArgumentException: Unable to resolve the Client Registration Identifier.
                        It must be provided via @RegisteredOAuth2AuthorizedClient("client1")
                        or @RegisteredOAuth2AuthorizedClient(registrationId = "client1").] with root cause
                        但是，可以直接使用这个代码来配置（使用默认配置） .oauth2Login(withDefaults())，clientRegistrationRepository 会自动注入到配置中
                */
                .oauth2Login(oauth2Login -> oauth2Login
                        .clientRegistrationRepository(clientRegistrationRepository)
                        .authorizationEndpoint(authorization -> authorization
                                // Customizing the Authorization Request
                                .authorizationRequestResolver(authorizationRequestResolver(clientRegistrationRepository))
                        )
                )
                .oauth2Login().and()
                .oauth2Login(withDefaults())
                .oauth2Client(oauth2Client -> oauth2Client
                                .clientRegistrationRepository(clientRegistrationRepository)

                //                .authorizedClientRepository(authorizedClientRepository())
                //                .authorizedClientService(authorizedClientService())
                                .authorizationCodeGrant(codeGrant -> codeGrant
                                        // Authorization Grants: Example 1. Configure a custom implementation of AuthorizationRequestRepository
//                                        .authorizationRequestRepository(authorizationRequestRepository())
//                                        .authorizationRequestResolver(authorizationRequestResolver())
                                        // Authorization Grants: Example 2. Configure a custom Access Token Response
                                        .accessTokenResponseClient(accessTokenResponseClient())

                                )
                )
        ;
    }

    /**
     * Customizing the Authorization Request
     * One of the primary use cases an OAuth2AuthorizationRequestResolver can realize is the ability to customize the Authorization Request with additional parameters above the standard parameters defined in the OAuth 2.0 Authorization Framework.
     * For example, OpenID Connect defines additional OAuth 2.0 request parameters for the Authorization Code Flow extending from the standard parameters defined in the OAuth 2.0 Authorization Framework. One of those extended parameters is the prompt parameter.
     * OPTIONAL. Space delimited, case sensitive list of ASCII string values that specifies whether the Authorization Server prompts the End-User for reauthentication and consent. The defined values are: none, login, consent, select_account
     * The following example shows how to configure the DefaultOAuth2AuthorizationRequestResolver with a Consumer<OAuth2AuthorizationRequest.Builder> that customizes the Authorization Request for oauth2Login(), by including the request parameter prompt=consent.
     *
     * @param clientRegistrationRepository
     * @return
     */
    private OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
        DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
                new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, "/oauth2/authorization");
        authorizationRequestResolver.setAuthorizationRequestCustomizer(authorizationRequestCustomizer());
        return authorizationRequestResolver;
    }

    // OAuth2AuthorizationRequest.Builder使用了生成器模式
    private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
        return customizer -> customizer
                .additionalParameters(params -> params.put("prompt", "consent"));
    }

    /*
    if your requirements are more advanced, you can take full control in building the Authorization Request URI by simply overriding the OAuth2AuthorizationRequest.authorizationRequestUri property.
    OAuth2AuthorizationRequest.Builder.build() constructs the OAuth2AuthorizationRequest.authorizationRequestUri, which represents the Authorization Request URI including all query parameters using the application/x-www-form-urlencoded format.
    The following example shows a variation of authorizationRequestCustomizer() from the preceding example, and instead overrides the OAuth2AuthorizationRequest.authorizationRequestUri property.
     */

    private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer2() {
        return customizer -> customizer
                .authorizationRequestUri(uriBuilder -> uriBuilder
                        .queryParam("prompt", "consent").build());
    }

    /**
     * Example 2. Configure a custom Access Token Response
     *
     */
    private OAuth2AccessTokenResponseClient accessTokenResponseClient() {

        DefaultAuthorizationCodeTokenResponseClient tokenResponseClient =
                new DefaultAuthorizationCodeTokenResponseClient();

        // 1 customize the pre-processing of the Token Request
        OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
        /*
         1.1 customize only the parameters of the request
         To customize only the parameters of the request, you can provide
            OAuth2AuthorizationCodeGrantRequestEntityConverter.setParametersConverter() with a custom
            Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> to completely override the parameters sent with the request.
            This is often simpler than constructing a RequestEntity directly.
         */
//        requestEntityConverter.setParametersConverter(converter1);
        /*
        1.2 If you prefer to only add additional parameters, you can provide
        OAuth2AuthorizationCodeGrantRequestEntityConverter.addParametersConverter() with a custom
        Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> which constructs an aggregate Converter.
         */
//        requestEntityConverter.addParametersConverter(converter2);

        tokenResponseClient.setRequestEntityConverter(requestEntityConverter);

        /*
         2 On the other end, if you need to customize the post-handling of the Token Response,
             you will need to provide DefaultAuthorizationCodeTokenResponseClient.setRestOperations() with a custom configured RestOperations.
             The default RestOperations is configured as follows:
         */
        RestTemplate restTemplate = new RestTemplate(Arrays.asList(
                new FormHttpMessageConverter(),
                new OAuth2AccessTokenResponseHttpMessageConverter()));
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
        tokenResponseClient.setRestOperations(restTemplate);
        return tokenResponseClient;
    }

    ////////////////////////////////////////////////////////////////////
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
